/*
 * Server.java
 *
 * Created on 4 de Agosto de 2007, 10:05
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package org.gpp.proj1.logic.core.server;

import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.rmi.RemoteException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

import org.gpp.proj1.communication.CommunicationException;
import org.gpp.proj1.communication.listener.OfflineClientListener;
import org.gpp.proj1.communication.listener.RequestListener;
import org.gpp.proj1.communication.manager.NetworkManager;
import org.gpp.proj1.communication.manager.RMINetworkManager;
import org.gpp.proj1.communication.protocol.Message;
import org.gpp.proj1.communication.protocol.ProtocolMessageFactory;
import org.gpp.proj1.logic.core.board.BoardPositionGenerator;
import org.gpp.proj1.logic.core.board.RobotPosition;
import org.gpp.proj1.logic.metadata.RobotPartCategory;
import org.gpp.proj1.map.GameMap;
import org.gpp.proj1.map.MalformedMapFileException;
import org.gpp.proj1.map.parser.MapFileParser;
import org.gpp.proj1.util.io.FileStringExtractor;

import com.jme.util.LoggingSystem;

/**
 * Class that implements the game server
 *
 * @author Gustavo Farias
 * @author Paolo Victor, paolovictor@gmail.com
 */
public class Server implements OfflineClientListener, RequestListener {

	/**
	 * The maximum number of incoming messages in the incoming messages
	 * queue
	 */
	private static final int INCOMING_MESSAGES_CAPACITY = 100;

	/**
	 * The server alias. Used for server announcements
	 */
	private static final String SERVER_ALIAS = "Game Server";

	/**
	 * The minimum number of players necessary to start the game
	 */
	private static final int MIN_PLAYERS = 1;

	/**
	 * The server name, used as a human-readable server identification
	 */
	private String serverName;
	
	/**
	 * The maximum number of players allowed on this server
	 */
	private int maxPlayers;
	
	/**
	 * The name of the map used by this server, used just for identification
	 */
	private String mapName;
		
	/**
	 * The port that the server runs on
	 */
	private int port;
	
	/**
	 * The ClientId -> Client Alias mapping
	 */
	private Map<String, String> aliasMap;
	
	/**
	 * The netman used by the server to handle incoming connections and
	 * messages
	 */
	private NetworkManager networkManager;

	/**
	 * The incoming messages queue
	 */
	private BlockingQueue<Message> incomingMessages;

	/**
	 * A thread that takes and handles requests from the incoming messages
	 * queue
	 */
	private ServerRequestHandler requestHandler;

	/**
	 * The current server phase. May be: LOBBY, INIT, INGAME and ENDGAME 
	 */
	private ServerPhases currentPhase; 

	/**
	 * Set with the connected clients that are ready. Note that
	 * it -must- be a subset of connectedClients
	 */
	private Set<String> readyClients;

	/**
	 * Set with the currently connected client ids
	 */
	private Set<String> connectedClients;
	
	/**
	 * The clientId -> robotData mapping
	 */
	private Map<String, String[]> rawRobotDataMap;
	
	private Map<String, Map<String, Map<RobotPartCategory, String>>> robotMapping;

	private GameMap gameMap;

	private String gameMapString;

	private int currentTurn;
	
	private List<String> turnSequence;
	
	private List<String> robotTurnSequence;
	
	private String currentTurnRobot;

	/**
	 * Creates a new server instance
	 * 
	 * @param name The server name
	 * @param maxPlayers The maximum number of players
	 * @param mapName The map name
	 * @param mapFile The map file
	 * @param port The port on which the server will listen to incoming connections
	 */
	public Server( String name, int maxPlayers, String mapName, File mapFile, int port ) {
		
		try {
			this.gameMapString = FileStringExtractor.readFile( mapFile );
			this.gameMap = MapFileParser.parseFileFromString( gameMapString );
		} catch (IOException e) {
			throw new IllegalArgumentException("Invalid or missing map file", e);
		} catch (MalformedMapFileException e) {
			throw new IllegalArgumentException("Invalid or missing map file", e);
		}
		
		if (name == null || name.trim().equals("")) {
			throw new IllegalArgumentException("Missing server name.");
		}
		// if illegal port, exception is thrown
		new InetSocketAddress( port );

		this.serverName = name;
		this.maxPlayers = maxPlayers;
		this.mapName = mapName;
		this.port = port;

		this.currentPhase = ServerPhases.LOBBY;

		this.connectedClients = new TreeSet<String>();
		this.readyClients = new TreeSet<String>();
		this.turnSequence = new LinkedList<String>();
		this.robotTurnSequence = new LinkedList<String>();
		
		this.aliasMap = new HashMap<String, String>();
		this.rawRobotDataMap = new HashMap<String, String[]>();
		this.robotMapping = new HashMap<String, Map<String,Map<RobotPartCategory,String>>>();

		this.incomingMessages = new ArrayBlockingQueue<Message>( INCOMING_MESSAGES_CAPACITY );
		
		this.currentTurn = 0;
	}

	/**
	 * Starts the server, initializing its network manager and
	 * request handler
	 * 
	 * @throws CommunicationException
	 */
	public void start() throws CommunicationException {

		try {
			this.networkManager = new RMINetworkManager( this.port );
			this.networkManager.registerRequestListener( this );
			this.networkManager.registerOfflineClientListener( this );

			this.requestHandler = new ServerRequestHandler( this, this.networkManager, this.incomingMessages );

			LoggingSystem.getLogger().info( "Starting request handler" );
			new Thread( requestHandler ).start();

			LoggingSystem.getLogger().info( "Server up and running" );		
			LoggingSystem.getLogger().info( "Starting LOBBY phase" );
		} catch (RemoteException e) {
			throw new CommunicationException( "Could not start server", e );
		}
	}

	/**
	 * Stops the server. It kills the request handler thread
	 * and stops the network manager, that notifies its clients
	 * upon disconnection
	 */
	public void stop() {

		this.requestHandler.stopRunning();
		this.networkManager.stop();
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.communication.listener.OfflineClientListener#clientIsOffline(java.lang.String)
	 */
	public void clientIsOffline( String clientId ) {

		this.aliasMap.remove( clientId );
		this.readyClients.remove( clientId );
		this.connectedClients.remove( clientId );
		this.turnSequence.remove( clientId );
		
		this.networkManager.broadcastMessage( ProtocolMessageFactory.buildClientOfflineMessage( clientId ) );
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.communication.listener.RequestListener#requestReceived(org.gpp.proj1.communication.protocol.Message)
	 */
	public void requestReceived( Message message ) {

		this.incomingMessages.offer( message );
	}
	
	/**
	 * Changes the game map, broadcasting a message to notify the change
	 * @param mapName The map name
	 */
	public void changeMap(String mapName) {

		this.mapName = mapName;
		
		this.networkManager.broadcastMessage( ProtocolMessageFactory.buildChangeMapMessage( mapName ) );	
	}

	/**
	 * @return <code>true</code> if the server may start a game, which means that there are ate least
	 * two ready players in the lobby
	 */
	public boolean mayInitGame() {

		return this.currentPhase.equals( ServerPhases.LOBBY ) && this.readyClients.size() >= MIN_PLAYERS;
	}
	
	/**
	 * Kicks a client from the server
	 * @param clientId
	 */
	public void kickClient(String clientId) {
		Message message = ProtocolMessageFactory.buildChatMessage( SERVER_ALIAS, "Kicking " + this.aliasMap.get( clientId ) );
		
		this.networkManager.broadcastMessage( message );
		
		message = ProtocolMessageFactory.buildClientOfflineMessage( clientId );
		
		this.networkManager.broadcastMessage( message );
		
		this.networkManager.disconnectClient( clientId );
		

	}

	public void initGame() throws ServerPhaseException {
		
		if( ! this.mayInitGame() ) {
			throw new ServerPhaseException( "Not enough players" );
		}
		
		if( ! this.currentPhase.equals( ServerPhases.LOBBY ) ) {
			throw new ServerPhaseException( "Invalid phase transition" );
		}
		
		this.currentPhase = ServerPhases.INIT;

		this.readyClients.clear();
		
		this.networkManager.broadcastMessage( ProtocolMessageFactory.buildStartInitPhase() );
	}
	
	/**
	 * Used by the server to check if it may send the map data to the users. The
	 * server may only send the map data if all clients have sent their respective
	 * robots data
	 * 
	 * @return
	 */
	private boolean checkAllPlayerSentRobots() {
		
		for( String clientId : this.connectedClients ) {
			if( ! this.rawRobotDataMap.containsKey( clientId ) ) {
				return false;
			}
		}
		
		return true;
	}

	public void addOnlineClient(String clientId, String clientAlias ) {

		this.connectedClients.add( clientId );
		this.turnSequence.add( clientId );
		
		this.aliasMap.put( clientId, clientAlias );
		
		// Sending server name
		networkManager.sendMessage( clientId, ProtocolMessageFactory.buildServerNameMessage( serverName ) );

		// Send map name
		networkManager.sendMessage( clientId, ProtocolMessageFactory.buildChangeMapMessage( mapName ) );

		// Send "Who's here" message
		networkManager.broadcastMessage( ProtocolMessageFactory.buildWhosHereMessage( aliasMap, readyClients ) );
	}

	public ServerPhases getCurrentPhase() {

		return this.currentPhase;
	}

	public void toggleReadiness(String senderId ) {
		if( connectedClients.contains( senderId ) ) {
			LoggingSystem.getLogger().info( "Client toggling readiness: " + senderId );

			if( readyClients.contains( senderId ) ) {
				// Client is ready, removing from list
				readyClients.remove( senderId );
			} else {
				// Adding client to ready list and checking if the game may start
				readyClients.add( senderId );
			}

			// Send "Who's here" message
			networkManager.broadcastMessage( ProtocolMessageFactory.buildWhosHereMessage( aliasMap, readyClients ) );
		}
	}

	public void playerHasSentRobotData(String senderId, String[] rawRobotDataEntry, Map<String, Map<RobotPartCategory, String>> robotMappingEntry) {

		synchronized ( rawRobotDataMap ) {

			rawRobotDataMap.put( senderId, rawRobotDataEntry );
			robotMapping.put( senderId, robotMappingEntry );

			if( checkAllPlayerSentRobots() ) {

				// Sending player robot info
				for( String clientID : rawRobotDataMap.keySet() ) {
					Message reply = ProtocolMessageFactory.buildPlayerRobotDataMessage( clientID, rawRobotDataMap.get( clientID ) );
					networkManager.broadcastMessage( reply );
				}

				// Sending map data
				networkManager.broadcastMessage( ProtocolMessageFactory.buildMapDataMessage( gameMapString ) );

				// Generating board positions
				Map<String, Map<String, RobotPosition>> boardPositions = generateGameBoardPositions();

				// Sending positions to clients
				networkManager.broadcastMessage( ProtocolMessageFactory.buildGameBoardMessage( boardPositions ) );
			}
		}
	}

	private Map<String, Map<String, RobotPosition>> generateGameBoardPositions() {
		// Generating board positions
		int robotCount = countRobots();
		List<RobotPosition> robotPositions = BoardPositionGenerator.generatePositions( robotCount, gameMap );

		// Mapping positions
		Map<String, Map<String, RobotPosition>> robotPositionMapping = new HashMap<String, Map<String,RobotPosition>>();
		for( String clientId : robotMapping.keySet() ) {

			Map<String, RobotPosition> mapping = new HashMap<String, RobotPosition>();
			for( String robot : robotMapping.get( clientId ).keySet() ) {

				mapping.put( robot, robotPositions.remove( 0 ) );
			}
			robotPositionMapping.put( clientId, mapping );
		}

		return robotPositionMapping;
	}

	private int countRobots() {

		int count = 0;
		for( String clientId : robotMapping.keySet() ) {

			count += robotMapping.get( clientId ).size();
		}

		return count;
	}

	public boolean mayStartGame() {
		
		return this.currentPhase.equals( ServerPhases.INIT ) && this.readyClients.equals( this.connectedClients );
	}

	public void startGame() {
	
		LoggingSystem.getLogger().warning( "Starting game on server." );
		
		this.currentPhase = ServerPhases.INGAME;
		
		this.networkManager.broadcastMessage( ProtocolMessageFactory.buildStartInGamePhase() );
		
		this.loadRobotTurnSequence();
	}

	public void sendNextTurn(String clientId) {
		
		String next = turnSequence.get( this.currentTurn );
		
		if( next.equals( clientId ) ) { 
			this.networkManager.sendMessage( clientId, ProtocolMessageFactory.buildYourTurnMessage() );
		} else {
			this.networkManager.sendMessage( clientId, ProtocolMessageFactory.buildCurrentTurnMessage( next ) );
		}
		
		this.networkManager.sendMessage( clientId, ProtocolMessageFactory.buildCurrentTurnRobotMessage( this.currentTurnRobot ) );
	}

	private void nextTurn() {
		this.currentTurn++;
		this.currentTurn %= turnSequence.size();
		
		this.loadRobotTurnSequence();
	}

	private void loadRobotTurnSequence() {	
		
		this.robotTurnSequence.clear();
		this.robotTurnSequence.addAll( this.robotMapping.get( turnSequence.get( this.currentTurn ) ).keySet() );
		
		this.currentTurnRobot = this.robotTurnSequence.remove( 0 );
	}

	public void playerEndedTurn() {
		
		if( this.robotTurnSequence.isEmpty() ) {
			this.nextTurn();
		} else {
			this.currentTurnRobot = this.robotTurnSequence.remove( 0 );	
		}
		
		this.networkManager.broadcastMessage( ProtocolMessageFactory.buildEndTurn() );		
	}

	public void playerMovedRobot(String client, String robot, int x, int y) {

		this.networkManager.broadcastMessage( ProtocolMessageFactory.buildPlayerMovedRobot( client, robot, x, y ) );
	}
	

	public void playerAttacked(String client, String robot, int x, int y, String part, String attack) {
		
		this.networkManager.broadcastMessage( ProtocolMessageFactory.buildPlayerAttacked( client, robot, x, y, part, attack ) );
		
	}
}